C++ on MSVC講習/条件分岐1 - ifいふ

あらすじと概要

前回は整数と小数について少し詳細に解説しました。
今回はいかにもなプログラミングが出来るようになる、if文について解説します。

重要語

複合文

複数の文を1つとして扱うことが出来る文

ネスト

ある構造を再帰的に記述すること

スコープ

識別子を使用できる範囲についてのコード上での範囲

ライフタイム

変数自体が使える範囲

true/false

真/偽、あるいは正しい/正しくない

std::boolalphaぶーるあるふぁ

true/falseを文字列として出力するためのマニピュレータ

==

いこーる、比較演算子の1つ

!=

のっといこーる、比較演算子の1つ

<

しょうなり、比較演算子の1つ

<=

しょうなりいこーる、比較演算子の1つ

>

だいなり、比較演算子の1つ

>=

だいなりいこーる、比較演算子の1つ

!

論理否定演算子

&&

論理積演算子

||

論理和演算子

短絡評価

論理積/論理和演算子で右辺の評価が省略されること

if文

条件によって実行する文を選択できる文

,

コンマ演算子

複合文

if文の前にいくらか説明をしたいので、次のサンプルコードを実行してみてください。
複合文
#include <iostream>

int main()
{
    int a = 1;

    std::cout << "1\n"
        << "    a " << a << "\n\n";

    {
        std::cout << "2\n"
            << "    a " << a << "\n\n";

        int a = 2;
        int b = 4;

        {

            std::cout << "3\n"
                << "    a " << a << "\n"
                << "    b " << b << "\n\n";

        }

        std::cout << "4\n"
            << "    a " << a << "\n"
            << "    b " << b << "\n\n";
    }

    std::cout << "5\n"
        << "  a " << a << "\n";
}

実行結果
1
  a 1

2
  a 1

3
  a 2
  b 4

4
  a 2
  b 4

5
  a 1

解説

少し出力の量が多いので困惑してしまうかもしれません。

複合文

main関数内の{}が、複数の文を1つとして扱うことが出来る、複合文です。
複合文の中に複数文を書くことが出来、それらをまとめて1つの文と扱えるのです。
複合文は複文やブロックとも呼ばれますが、文であり、複合文はネスト出来ます。
main関数の{}と複合文は別なものですが、どちらもスコープを形成します。
複合文
{ 複数の文(opt) }

ネスト

ネストは「入れ子」などとも言われ、ある構造を再帰的に記述することです。
あるいは、あるものの中に、それと似たようなものが記述されている状態のことです。
複合文なら、{ { } }のように、複合文の中に複合文があることを指します。

スコープ

スコープとは、識別子を使用できる範囲についてのコード上での範囲のことです。
スコープは関数や複合文、他にもいくつかの構文で形成され、ネスト出来ます。
実は、識別子は宣言をすることで、宣言をしたスコープへ導入されていたのです。
そして、導入された識別子は、導入先とネストするスコープで使用できるのです。
サンプルコードだと、変数bが分かりやすいかと思います。

スコープと識別子

識別子について、同じスコープに同じ識別子を複数導入することは出来ないのです。
これは、同じスコープに同じ識別子が複数あると、どれがどれだか区別できなくなるからです。
ただし、スコープがネストしている場合は、それぞれのスコープで同じ識別子を導入できます。
その時、より外側のスコープの識別子は、より内側のスコープの識別子によって隠されます。
サンプルコードだと、変数aにおいてまさにその状態になっています。

スコープと変数のライフタイム

変数の識別子もやはり識別子なので、変数が使用できる範囲はスコープで制限されます。
変数自体が使える範囲のことを変数のライフタイム、あるいは変数のスコープと呼びます。
ライフタイムの始まりは、やはり宣言をした後からということになります。
一方、ライフタイムの終わりは、変数を宣言したスコープの終わりと一致するのです。

解説1 - スコープを考える

以上のことから、今回考えるべきなのは、main関数、main関数の中の複合文、
そして、そのmain関数の中の複合文の複合文の、3つが持つスコープになります。
これら3つのスコープを、便宜的にA/B/Cとして表すことにします。

解説2 - 変数のライフタイム

ABで、変数aが2つ、Bで変数bが宣言されていることがわかるでしょうか。
bについては、Bでの宣言以降と、Cで使用できることになります。
そして、Aaは、Aでの宣言以降、BではBaが宣言されていない部分で、
一方、Baは、Bでの宣言以降とCで使用できるということになります。
ですから、aの出力は順番に、1, 1, 2, 2, 1となるのです。

真偽値とbool

次に、数学でも出てくる真偽の値について解説していきます。
真偽値とbool
#include <iostream>
#include <string>

int main()
{

    std::string s = "string";

    std::cout
        << "1 : " << true << " " << false << "\n\n"

        << std::boolalpha
        << "2 : " << true << " " << false << "\n"
        << "3 : " << (bool)1 << " " << (bool)0 << "\n"
        << "4 : " << (int)true << " " << (int)false << "\n\n"

        << "5 : " << (s == s) << " " << (s != s) << "\n"
        << "6 : " << (0 < 1) << " " << (1 <= 0) << "\n"
        << "7 : " << (true > false) << " " << (false >= true) << "\n\n"

        << "8 : " << (!true) << " " << (!false) << "\n"
        << "9 : " << (true && true) << " " << (true && false) << " " << (false && true) << " " << (false && false) << "\n"
        << "10: " << (true || true) << " " << (true || false) << " " << (false || true) << " " << (false || false) << "\n"
        ;
}

実行結果
1 : 1 0

2 : true false
3 : true false
4 : 1 0

5 : true false
6 : true false
7 : true false

8 : false true
9 : true false false false
10: true true true false

解説

なにやら知らない文字がたくさんありますが、実際は簡単です。

boolとリテラル(1-4, 7-10)

boolぶーるというのは、変数の型です。booltrueとぅるーfalseふぉるすという2つの値のみ取ります。
trueが真/falseが偽で、平易に言えば、正しい/正しくないという感じです。
boolは、真偽型ですが、同時に整数型にも分類され、大抵は8bitの整数型です。
なお、理論上は1bitでいいのですが、規格の制限で「大抵8bit」になっています。

bool値と整数小数の関係(1,3-4,7)

実は、C++ではbool値と整数小数は、暗黙の型変換が行われる関係にあります。
boolから変換される時は、false0true1に変換されます。
逆の場合は、0false非0trueに変換されます。
この規則は、キャストを行ったときも同様の規則で変換されます。
一部を除き、bool値は計算されるときは数値に変わってしまうことに注意しましょう。

boolとstd::coutとstd::boolalphaぶーるあるふぁ(1-10)

std::coutを用いて出力する際、標準だと整数になって出力されてしまいます。
当然、整数に変換される際は暗黙の型変換と同様の規則で整数に変換されます。
truefalseという文字列で出力したいときは、std::boolalphaを使います。
std::boolalphaもマニピュレータで、純粋にこれを出力すればよいです。
std::boolalphaは、#include <iostream>のみで使用することが出来ます。

等値演算子と比較演算子(5-7)

さて、数学でおなじみの= ≠ < ≦ > ≧は、== != < <= > >=と対応します。
これらの演算子の結果は、boolになり、式が正しければtrue、そうでなければfalseです。
これらの6種類の演算子は、まとめて比較演算子と呼ばれることがあります。
また、前者2つを等値演算子、後者4つを比較演算子と呼ぶ場合もあります。
それはともかく、代入と==を間違えないように細心の注意を払いましょう。

比較演算子(上数学記号、読み、下C++記号、意味)

 いこーる

== 両辺が等しいか

 のっといこーる

!= 両辺が等しくないか

 しょうなり

< 左辺より右辺が大きいか

 しょうなりいこーる

<= 左辺より右辺が大きいか、あるいは等しいか

 だいなり

> 右辺より左辺が大きいか

 だいなりいこーる

>= 右辺より左辺が大きいか、あるいは等しいか

文字列に対しての比較演算子(5)

サンプルコードでは、std::stirngに対しても比較演算子を適用していますが、
実は文字列リテラル同士だと、文字列の内容で比較することは出来ないのです。
なので、文字列の内容を比較したい場合には、右辺左辺どちらかをstd::stirngにしましょう。
std::stringは文字列リテラルをキャストする形式でも変換できるのでそれでもいいでしょう。

論理演算子(8-10)

比較演算子で条件式を立てられますが、組み合わせる時には論理演算子を使います。
論理演算子は! && ||の3つがあり、それぞれ、論理否定/論理積/論理和演算子と言います。
論理否定は前置の単項演算子なので、! bool値に評価される値と使います。
論理積と論理和はtruefalse10の計算だとして考えると理解できるでしょう。
なお、その演算結果から、「かつ」/「または」の意味をもち、そう呼ばれることがあります。

論理演算子(上C++記号、意味、下効果)

! 論理否定

truefalseを否定(反転) !truefalse!falsetrue

&& 論理積 「かつ」とも

truetrueのみtrue、それ以外はfalse

|| 論理和 「または」とも

falsefalseのみfalse、それ以外はtrue

論理積の演算結果

true && true

true

true && false

false

false && true

false

false && false

false

論理和の演算結果

true && true

true

true && false

true

false && true

true

false && false

false

論理積と論理和の短絡評価(9,10)

論理積と論理和の演算結果を見て勘の鋭い人がもう気付いたかもしれません。
論理和/論理積はそれぞれ、左辺がfalse/trueだと結果はfalse/trueであるのです。
右辺に関わらず結果が決まるため、右辺を評価しません。これを短絡評価と言います。

解説

サンプルコードの順に、必要な内容を解説しています。
サンプルコードを上から順に確認して暗算して出力と照らし合わせて下さい。
なお、タイトルにある番号は、出力の番号と対応しています。

if文

それではif文を扱ってみましょう。
注意点があるので、まずそれを確認してからコードを実行してみましょう。

言語規格のバージョンについての注意

C++の規格もバージョンがあり、現在3年ごとに改訂されています。
Visual Studioは2021/7/31現在、C++14/C++17/Working Draftが選べます。
そして、標準設定だとC++14になっているので、C++17に変更してください。
プロジェクト>○○のプロパティ>構成プロパティ>C/C++>言語>C++ 言語標準を確認して
ISO C++17 標準(/std:c++17)かそれよりも新しいのを選択します。
これ以降も上記と同様に、最も新しい完全に実装された言語標準を選択してください。

少し詳しい人向け

Visual Studioの設定をあまり変更してないので、cmd.exeとWindows-31Jを使っています。
なので/utf-8付けたり、system("chcp 65001")したりしないでください。
if文
#include <iostream>
#include <string>

int main()
{
    // 常に真文が実行される
    if (int num{ 1 }) std::cout << "数学の定期テスト何点でした?\n"; else int dummy = 0;

    if (int num; (std::cin >> num), num < 0 || num > 100)
    {
        std::cout << "受けていないですね?\n";
    }
    else
    {
        if (num >= 95)      /*評価 10 */ std::cout << "素晴らしい!\n";
        else if (num >= 75) /*評価 9,8*/ std::cout << "よくできました。\n";
        else if (num >= 65) /*評価 7  */ { std::cout << "よくがんばりました。\n"; }
        else if (num >= 55) /*評価 6  */ { std::cout << "もうすこしがんばりましょう。\n"; }
        else
        {
            std::cout << "中学生ですか?中学生ならyを入力してください。\n";
            std::string s; std::cin >> s;

            if (bool flag{ s == "y" })
            {
                if (num >= 45)
                {   /*評価 5  */
                    std::cout << "ぎりぎりですね。がんばりましょう。\n";
                }
                else
                {   /*赤点    */
                    std::cout << "もっとがんばりましょう。\n";
                }
            }
            else
            {
                if (num >= 45)
                {   /*評価 5  */
                    std::cout << "もうすこしがんばりましょう。\n";
                }
                else if (num >= 35)
                {   /*評価 4  */
                    std::cout << "ぎりぎりですね。がんばりましょう。\n";
                }
                else
                {   /*赤点    */
                    std::cout << "もっとがんばりましょう。\n";
                }
            }
        }
    }
}

実行結果について

実行例を見ていくのは大変なので、下の表を見て下さい。
なお、54点以下で中学生かどうかのチェックが入ります。
中学生ならy、高校生ならそれ以外を入力します。

点数と出力の関係(中学生)

100~95, 評価 10

素晴らしい!

94~75, 評価 9~8

よくできました。

74~65, 評価 7

よくがんばりました

64~55, 評価 6

もうすこしがんばりましょう。

54~45, 評価 5

ぎりぎりですね。がんばりましょう。

44~0, 評価 4~0, 赤点

もっとがんばりましょう。

上記以外

受けていないですね?

点数と出力の関係(高校生)

100~95, 評価 10

素晴らしい!

94~75, 評価 9~8

よくできました。

74~65, 評価 7

よくがんばりました

64~45, 評価 6~5

もうすこしがんばりましょう。

44~35, 評価 4

ぎりぎりですね。がんばりましょう。

34~0, 評価 3~0, 赤点

もっとがんばりましょう。

上記以外

受けていないですね?
実行結果例1
数学の定期テスト何点でした?
$ -1
受けていないですね?
 
実行結果例2
数学の定期テスト何点でした?
$ 100
素晴らしい!
 
実行結果例3
数学の定期テスト何点でした?
$ 80
よくできました。
 
実行結果例4
数学の定期テスト何点でした?
$ 70
よくがんばりました。
 
実行結果例5
数学の定期テスト何点でした?
$ 60
もうすこしがんばりましょう。
 
実行結果例6
数学の定期テスト何点でした?
$ 50
中学生ですか?中学生ならyを入力してください。
$ y
ぎりぎりですね。がんばりましょう。
 
実行結果例7
数学の定期テスト何点でした?
$ 50
中学生ですか?中学生ならyを入力してください。
$ n
もうすこしがんばりましょう。
 
実行結果例8
数学の定期テスト何点でした?
$ 40
中学生ですか?中学生ならyを入力してください。
$ y
もっとがんばりましょう。
 
実行結果例9
数学の定期テスト何点でした?
$ 40
中学生ですか?中学生ならyを入力してください。
$ n
ぎりぎりですね。がんばりましょう。
 
実行結果例10
数学の定期テスト何点でした?
$ 30
中学生ですか?中学生ならyを入力してください。
$ n
もっとがんばりましょう。
 

解説

解説行きましょう。

if文

if文は、条件によって、真文偽文の実行を選ぶことが出来ます。
これによって、実行時に実行する内容を変えることが出来るようになります。
if
if ( 初期化文(opt) 条件 ) 真文
if ( 初期化文(opt) 条件 ) 真文 else 偽文

初期化文

C++17で追加された初期化文では、;で終わるような1つの文を書くことが出来ます。
具体的には、変数宣言や、式文、即ち式に;を付けたもの、などが記述できます。
なお、ここで宣言された変数は、if文全体をスコープとして使用することが出来ます。
すなわち、else以下が無ければ真文まで、あれば偽文までをスコープに持つことになります。

条件

条件は、boolに評価される式、又は変数の宣言を書くことが出来ます。
宣言の場合、;は必要なく、型は配列ではなく、boolに変換できるものになります。
配列は、今後の記事で説明する、変数を複数並べて用意する機能のことです。
そして、初期化は=/{}/={}による形式のみで、()の形式は許可されません。
なお、ここで宣言された変数も、初期化文の場合と同様、if文全体をスコープに持ちます。

よくある間違い1

条件のうち、「boolに評価される式」に間違えやすいところが隠れています。
解説したように、intやdoubleなどの値は暗黙のうちにboolに変換されてしまいます。
例えば、aをintとした時a == 1のつもりがa = 1としたとしましょう
この時、a = 11になったatrueになり、エラーは発生しません。
このように、意図しない動作になってしまう時があるので注意しましょう。

真文偽文

真文偽文は、共に単一の文を必要とし、おおむね複合文を使用します。
また、複合文でなくとも、1文で複合文であるようにスコープを持ちます。

if文の実行のされ方

if文はelseえるす以下があるかで実行のされ方が少しだけ変わります。
else以下がが無い場合、条件がtrueの時に真文が、falseの時にはif文は終了します。
else以下がある場合は、条件がtrueの時に真文が、falseの時には偽文を実行します。

よくある間違い2

例えば、if(b) a = 1; a = 2(aはint, bはbool)と書いたとしましょう。
この時、if文に含まれるのはa = 1;のみで、a = 2;は含まれません。
つまり、bの真偽によらず、a = 2;が実行されてしまうのです。
この観点からも真文偽文には複合文を使用するべきだと言えます。

else-if文

特別に名前を付けるほどではないですが、else-if文という言い方があります。
これは、偽文にif文を書き、if(){} else if(){}のような形になる書き方です。
偽文を複合文にし、その中にif文を書く時であれば、if() {} else { if(){} }
のようになるので、それに比べてネストしないため、きれいな形になります。

コンマ演算子

(std::cin >> num), num < 0 || num > 100,は、コンマ演算子です。
これは、左辺を評価し、次に右辺を評価して、右辺の評価結果を、全体の評価結果とします。
今回は、入力して、条件式を立てる、という2つのことを条件の中でするために使用しました。

練習問題

練習問題になります。

問題文

1行の計算式が与えられるので、その結果を出力してください。
与えられる計算式のパターンと対応する出力は以下の表の通りです。(原文ママ)
出典:EX6 - 電卓をつくろう
入力
$ A op B
制約
0 <= A,B <= 100
A,Bは整数
opは+,-,*,/,?,=,!のいずれか
出力
以下の表に従って出力(ただし、出力の最後に改行が必要)

入力(上)と出力(下)

A + B

A + Bの計算結果を出力

A - B

A - Bの計算結果を出力

A * B

A * Bの計算結果を出力

A / B

A / Bの計算結果を出力、小数点以下切り捨て、ゼロ除算はerrorと出力

A ? B

errorと出力

A = B

errorと出力

A ! B

errorと出力
回答例
回答例
#include <iostream>
#include <string>

int main()
{
    int a, b;
    std::string op;

    std::cin >> a >> op >> b;

    if (op == "+")
    {
        std::cout << a + b;
    }
    else if (op == "-")
    {
        std::cout << a - b;
    }
    else if (op == "*")
    {
        std::cout << a * b;
    }
    else if (op == "/")
    {
        if (b == 0)
        {
            std::cout << "error";
        }
        else
        {
            std::cout << a / b;
        }
    }
    else
    {
        std::cout << "error";
    }

    std::cout << "\n";
}

参照、出典

参照や出典です

参照

[stmt.pre]

https://timsong-cpp.github.io/cppwp/n4861/stmt.pre

[stmt.select]

https://timsong-cpp.github.io/cppwp/n4861/stmt.select#stmt.if

文 - cppreference.com

https://ja.cppreference.com/w/cpp/language/statements

スコープ - cppreference.com

https://ja.cppreference.com/w/cpp/language/scope

基本型 - cppreference.com

https://ja.cppreference.com/w/cpp/language/types

暗黙の変換 - cppreference.com

https://ja.cppreference.com/w/cpp/language/implicit_conversion

比較演算子 - cppreference.com

https://ja.cppreference.com/w/cpp/language/operator_comparison

論理演算子 - cppreference.com

https://ja.cppreference.com/w/cpp/language/operator_logical

if 文 - cppreference.com

https://ja.cppreference.com/w/cpp/language/if

if文とswitch文の条件式と初期化を分離 - cpprefjp C++日本語リファレンス

https://cpprefjp.github.io/lang/cpp17/selection_statements_with_initializer.html